Testing Flutter UI and Functionality
Testing Flutter UI and Functionality is the process of verifying that a Flutter application's user interface, widgets, interactions, navigation, state changes, validation, and business behavior work as expected. Flutter provides testing tools that allow developers to test individual pieces of logic, widgets, and complete application workflows.
What is Flutter UI Testing?
Flutter UI testing focuses on verifying how widgets are displayed and how they respond to user interactions. A UI test can check whether text is displayed, buttons can be tapped, text can be entered, lists can be scrolled, forms can be validated, and screens can navigate correctly.
What is Functionality Testing?
Functionality testing verifies whether an application's features produce the expected results. For example, a login feature should validate credentials, a counter should increase when a button is pressed, and a shopping cart should update when an item is added.
Why Test Flutter UI and Functionality?
- To verify that the UI displays the expected widgets.
- To confirm that user interactions work correctly.
- To verify application logic.
- To detect bugs early.
- To prevent existing features from breaking after code changes.
- To make refactoring safer.
- To verify loading, success, empty, and error states.
- To test navigation between screens.
- To improve application reliability.
- To automate repetitive testing tasks.
Types of Testing Used for Flutter UI and Functionality
| Testing Type | Main Purpose | Example |
|---|
| Unit Testing | Test individual functions, methods, or classes | Testing a price calculation |
| Widget Testing | Test individual widgets and UI interactions | Testing a login button |
| Integration Testing | Test complete application workflows | Login → Dashboard → Logout |
| Golden Testing | Compare rendered UI with reference images | Checking visual layout changes |
Flutter UI Testing Flow
Build Widget
↓
Find Widget
↓
Perform User Action
↓
Rebuild Widget
↓
Check Result
↓
Pass / Fail
Flutter Testing Packages
The flutter_test package is included with the Flutter SDK and provides APIs for widget testing. It includes WidgetTester, testWidgets(), Finders, and widget-specific Matchers.
dev_dependencies:
flutter_test:
sdk: flutter
For integration testing, Flutter provides the integration_test package.
dev_dependencies:
integration_test:
sdk: flutter
Widget Testing Basics
A widget test creates a test environment in which widgets can be built, located, interacted with, and verified. The basic process is:
- Create a widget to test.
- Create a
testWidgets() test.
- Build the widget with
pumpWidget().
- Find widgets using Finders.
- Perform interactions using
WidgetTester.
- Rebuild the widget using
pump() or pumpAndSettle().
- Verify the expected result using
expect().
Basic UI Widget Example
import 'package:flutter/material.dart';
class WelcomeScreen extends StatelessWidget {
const WelcomeScreen({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: Text(
'Welcome to Flutter',
),
),
),
);
}
}
Testing Text on the Screen
Use find.text() to locate a Text widget displaying specific text.
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Welcome text is displayed', (tester) async {
await tester.pumpWidget(
const WelcomeScreen(),
);
expect(
find.text('Welcome to Flutter'),
findsOneWidget,
);
});
}
Understanding testWidgets()
The testWidgets() function creates a widget test and provides a WidgetTester that can build and interact with widgets in the test environment.
testWidgets('Test description', (tester) async {
// Build widget
// Interact with widget
// Verify result
});
Understanding WidgetTester
WidgetTester is used to interact with widgets during widget testing.
| Method | Purpose |
|---|
pumpWidget() | Builds and renders a widget. |
pump() | Schedules a frame and rebuilds the widget. |
pumpAndSettle() | Continues pumping until scheduled frames are complete. |
tap() | Simulates a tap. |
enterText() | Enters text into a text input. |
drag() | Simulates a drag gesture. |
scroll() | Simulates scrolling. |
scrollUntilVisible() | Scrolls until a widget becomes visible. |
pumpWidget()
pumpWidget() builds and renders the widget being tested.
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Text('Hello'),
),
),
);
pump()
After an interaction changes state, the test environment may need to rebuild the widget tree. The pump() method triggers a frame.
await tester.tap(
find.text('Increment'),
);
await tester.pump();
pumpAndSettle()
pumpAndSettle() repeatedly pumps frames until there are no more scheduled frames. It is useful for animations, navigation, and asynchronous UI updates.
await tester.pumpAndSettle();
Finding Widgets
Flutter provides Finder classes to locate widgets in the test environment.
Find by Text
find.text('Login')
Find by Type
find.byType(ElevatedButton)
Find by Key
find.byKey(
const ValueKey('loginButton'),
)
Find by Widget Instance
find.byWidget(
const Text('Hello'),
)
Common Finders
| Finder | Example | Purpose |
|---|
| Text | find.text('Login') | Find text displayed by a widget. |
| Type | find.byType(TextField) | Find widgets of a specific type. |
| Key | find.byKey(Key('login')) | Find a widget using a key. |
| Widget | find.byWidget(widget) | Find a specific widget instance. |
| Icon | find.byIcon(Icons.add) | Find a specific icon. |
| Descendant | find.descendant(...) | Find widgets within another widget. |
Common Matchers
| Matcher | Meaning |
|---|
findsOneWidget | Exactly one matching widget exists. |
findsNothing | No matching widget exists. |
findsWidgets | One or more matching widgets exist. |
findsNWidgets(3) | Exactly three matching widgets exist. |
Testing Buttons
Buttons are one of the most important UI elements to test because they commonly trigger application functionality.
Button Example
class LoginButton extends StatelessWidget {
final VoidCallback onPressed;
const LoginButton({
super.key,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return ElevatedButton(
key: const ValueKey('loginButton'),
onPressed: onPressed,
child: const Text('Login'),
);
}
}
Button Test
testWidgets('Login button can be tapped', (tester) async {
bool clicked = false;
await tester.pumpWidget(
MaterialApp(
home: LoginButton(
onPressed: () {
clicked = true;
},
),
),
);
await tester.tap(
find.byKey(
const ValueKey('loginButton'),
),
);
await tester.pump();
expect(clicked, true);
});
Testing TextField
Text fields can be tested by entering text using the enterText() method.
testWidgets('User can enter email', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: TextField(
key: ValueKey('emailField'),
),
),
),
);
await tester.enterText(
find.byKey(
const ValueKey('emailField'),
),
'[email protected]',
);
expect(
find.text('[email protected]'),
findsOneWidget,
);
});
Testing Form Functionality
Forms should be tested for valid input, invalid input, required fields, validation messages, and successful submission.
Example Form
class LoginForm extends StatefulWidget {
const LoginForm({super.key});
@override
State createState() => _LoginFormState();
}
class _LoginFormState extends State {
final formKey = GlobalKey();
final emailController = TextEditingController();
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Form(
key: formKey,
child: Column(
children: [
TextFormField(
controller: emailController,
key: const ValueKey('emailField'),
decoration: const InputDecoration(
labelText: 'Email',
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Email is required';
}
return null;
},
),
ElevatedButton(
key: const ValueKey('submitButton'),
onPressed: () {
formKey.currentState!.validate();
},
child: const Text('Submit'),
),
],
),
),
),
);
}
}
Testing Empty Form Validation
testWidgets('Empty email shows validation error', (tester) async {
await tester.pumpWidget(
const LoginForm(),
);
await tester.tap(
find.byKey(
const ValueKey('submitButton'),
),
);
await tester.pump();
expect(
find.text('Email is required'),
findsOneWidget,
);
});
Testing Valid Form Input
testWidgets('Valid email is accepted', (tester) async {
await tester.pumpWidget(
const LoginForm(),
);
await tester.enterText(
find.byKey(
const ValueKey('emailField'),
),
'[email protected]',
);
await tester.tap(
find.byKey(
const ValueKey('submitButton'),
),
);
await tester.pump();
expect(
find.text('Email is required'),
findsNothing,
);
});
Testing Checkboxes
testWidgets('Checkbox changes state', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Checkbox(
value: false,
onChanged: null,
),
),
),
);
expect(
find.byType(Checkbox),
findsOneWidget,
);
});
For interactive checkbox behavior, use a StatefulWidget or state-management implementation that changes the value in response to the callback, then tap the checkbox and verify the updated UI.
Testing Switches
testWidgets('Switch is displayed', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Switch(
value: true,
onChanged: null,
),
),
),
);
expect(
find.byType(Switch),
findsOneWidget,
);
});
Testing Dropdowns
Dropdown functionality can be tested by opening the dropdown, selecting an option, and verifying the selected value.
await tester.tap(
find.byType(DropdownButton),
);
await tester.pumpAndSettle();
expect(
find.text('Option 1'),
findsWidgets,
);
Testing Navigation
Navigation testing verifies that a user can move from one screen to another and that the expected destination UI appears.
Navigation Example
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
key: const ValueKey('detailsButton'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const DetailsScreen(),
),
);
},
child: const Text('Open Details'),
),
),
);
}
}
class DetailsScreen extends StatelessWidget {
const DetailsScreen({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('Details Screen'),
),
);
}
}
Navigation Test
testWidgets('User can navigate to details', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: HomeScreen(),
),
);
await tester.tap(
find.byKey(
const ValueKey('detailsButton'),
),
);
await tester.pumpAndSettle();
expect(
find.text('Details Screen'),
findsOneWidget,
);
});
Testing Back Navigation
await tester.pageBack();
await tester.pumpAndSettle();
expect(
find.text('Home Screen'),
findsOneWidget,
);
Testing ListView
List-based interfaces should be tested to verify that expected items are displayed and that users can interact with the list.
class ProductList extends StatelessWidget {
const ProductList({super.key});
@override
Widget build(BuildContext context) {
final products = [
'Laptop',
'Phone',
'Tablet',
];
return MaterialApp(
home: Scaffold(
body: ListView(
children: products.map((product) {
return ListTile(
title: Text(product),
);
}).toList(),
),
),
);
}
}
Testing List Items
testWidgets('Product list displays products', (tester) async {
await tester.pumpWidget(
const ProductList(),
);
expect(
find.text('Laptop'),
findsOneWidget,
);
expect(
find.text('Phone'),
findsOneWidget,
);
expect(
find.text('Tablet'),
findsOneWidget,
);
});
Testing Scrolling
For long lists, a widget may not be visible until the list is scrolled. The scrollUntilVisible() method can scroll through a list until a target widget becomes visible.
final listFinder = find.byType(Scrollable);
final itemFinder = find.text('Product 50');
await tester.scrollUntilVisible(
itemFinder,
500,
scrollable: listFinder,
);
expect(
itemFinder,
findsOneWidget,
);
Testing Gestures
Flutter widget tests can simulate common user gestures such as taps and drags.
Tap
await tester.tap(
find.text('Submit'),
);
Drag
await tester.drag(
find.byType(ListView),
const Offset(0, -300),
);
Scroll
await tester.scroll(
find.byType(ListView),
-300,
);
Testing Loading State
Applications frequently display a progress indicator while waiting for asynchronous data.
testWidgets('Loading indicator is visible', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: CircularProgressIndicator(),
),
),
);
expect(
find.byType(CircularProgressIndicator),
findsOneWidget,
);
});
Testing Error State
testWidgets('Error message is displayed', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Text('Something went wrong'),
),
),
);
expect(
find.text('Something went wrong'),
findsOneWidget,
);
});
Testing Empty State
testWidgets('Empty state is displayed', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Text('No products found'),
),
),
);
expect(
find.text('No products found'),
findsOneWidget,
);
});
Testing Stateful Widgets
Stateful widgets should be tested by performing an action that changes state and then verifying that the UI reflects the new state.
testWidgets('Counter updates after tap', (tester) async {
await tester.pumpWidget(
const CounterApp(),
);
expect(
find.text('0'),
findsOneWidget,
);
await tester.tap(
find.byKey(
const ValueKey('incrementButton'),
),
);
await tester.pump();
expect(
find.text('1'),
findsOneWidget,
);
});
Testing Functionality Behind a Button
A button should not only appear on the screen; its functionality should also be verified.
testWidgets('Add button adds item', (tester) async {
await tester.pumpWidget(
const TodoApp(),
);
await tester.enterText(
find.byType(TextField),
'Learn Flutter',
);
await tester.tap(
find.byType(FloatingActionButton),
);
await tester.pump();
expect(
find.text('Learn Flutter'),
findsOneWidget,
);
});
Testing Todo Functionality
A common Flutter application feature is adding and removing todo items. This functionality can be tested through the same interactions a user performs.
testWidgets('Add and remove todo item', (tester) async {
await tester.pumpWidget(
const TodoApp(),
);
await tester.enterText(
find.byType(TextField),
'Learn Flutter',
);
await tester.tap(
find.byType(FloatingActionButton),
);
await tester.pump();
expect(
find.text('Learn Flutter'),
findsOneWidget,
);
await tester.drag(
find.text('Learn Flutter'),
const Offset(-500, 0),
);
await tester.pumpAndSettle();
expect(
find.text('Learn Flutter'),
findsNothing,
);
});
Testing Dialogs
Dialogs are important UI components that should be tested for appearance and user actions.
await tester.tap(
find.text('Delete'),
);
await tester.pumpAndSettle();
expect(
find.text('Are you sure?'),
findsOneWidget,
);
expect(
find.text('Cancel'),
findsOneWidget,
);
expect(
find.text('Confirm'),
findsOneWidget,
);
Testing SnackBars
testWidgets('Snackbar appears after save', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Text('Save Screen'),
),
),
);
final scaffoldState = tester.state(
find.byType(Scaffold),
);
scaffoldState.showSnackBar(
const SnackBar(
content: Text('Saved successfully'),
),
);
await tester.pump();
expect(
find.text('Saved successfully'),
findsOneWidget,
);
});
Testing Async Functionality
Asynchronous functionality includes API calls, database operations, timers, and other Future-based operations.
test('Async function returns expected value', () async {
final result = await loadUser();
expect(
result,
'John',
);
});
Testing FutureBuilder UI
testWidgets('FutureBuilder displays user data', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: UserScreen(),
),
);
await tester.pumpAndSettle();
expect(
find.text('John'),
findsOneWidget,
);
});
Testing API-Driven UI
When testing a UI that depends on an API, it is useful to control the data source so that tests are predictable.
abstract class UserService {
Future getUserName();
}
class FakeUserService implements UserService {
@override
Future getUserName() async {
return 'Test User';
}
}
The UI can use the fake service during testing instead of relying on a live server.
Testing Plugin-Based Functionality
Flutter plugins can contain native platform code. During ordinary Dart unit tests and widget tests, that native host implementation is generally not available. Calling a plugin directly from such tests can therefore cause a MissingPluginException.
A practical approach is to wrap plugin calls behind an application-owned service and replace that service with a fake implementation during tests.
abstract class StorageService {
Future saveUser(String name);
}
class FakeStorageService implements StorageService {
String? savedName;
@override
Future saveUser(String name) async {
savedName = name;
}
}
Testing UI States
A well-tested Flutter screen should consider different UI states.
Initial State
↓
Loading State
↓
+-------------------+
| |
Success Error
| |
↓ ↓
Show Data Show Error
|
↓
Empty State if no data
UI State Testing Table
| State | What to Verify |
|---|
| Initial | Initial widgets appear correctly. |
| Loading | Progress indicator is displayed. |
| Success | Expected data appears. |
| Empty | Empty-state message appears. |
| Error | Error message or error UI appears. |
Testing Accessibility-Related UI Behavior
UI testing should also consider whether important controls can be identified meaningfully. Keys can help automated tests locate specific widgets, while appropriate labels and semantic information can support accessibility.
Semantics(
label: 'Login button',
child: ElevatedButton(
onPressed: login,
child: const Text('Login'),
),
)
Testing Responsive UI
A Flutter interface may need to work across different screen sizes and orientations. Widget tests can be configured with different test surface sizes when verifying responsive behavior.
testWidgets('Responsive layout test', (tester) async {
await tester.binding.setSurfaceSize(
const Size(400, 800),
);
await tester.pumpWidget(
const MyResponsiveApp(),
);
expect(
find.byType(MyResponsiveApp),
findsOneWidget,
);
});
Testing Portrait and Landscape Behavior
Responsive applications should be checked in different orientations when orientation affects the UI.
Portrait
↓
Narrow Width
↓
Column Layout
Landscape
↓
Wide Width
↓
Row / Expanded Layout
Golden Testing
Golden tests compare a widget's rendered output against a reference image. They are useful for detecting unintended visual changes in UI.
testWidgets('Profile screen matches golden image', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: ProfileScreen(),
),
);
await expectLater(
find.byType(ProfileScreen),
matchesGoldenFile('profile_screen.png'),
);
});
Testing UI Properties
Tests can verify that widgets have expected properties and behavior.
final button = tester.widget(
find.byKey(
const ValueKey('submitButton'),
),
);
expect(
button.onPressed,
isNotNull,
);
Testing Widget Count
expect(
find.byType(ListTile),
findsNWidgets(5),
);
Testing Widget Absence
expect(
find.text('Error'),
findsNothing,
);
Testing Multiple Widgets
expect(
find.byType(Text),
findsWidgets,
);
Testing Login Functionality
Login is a common example of UI and functionality testing.
testWidgets('Login workflow', (tester) async {
await tester.pumpWidget(
const LoginScreen(),
);
await tester.enterText(
find.byKey(
const ValueKey('emailField'),
),
'[email protected]',
);
await tester.enterText(
find.byKey(
const ValueKey('passwordField'),
),
'password123',
);
await tester.tap(
find.byKey(
const ValueKey('loginButton'),
),
);
await tester.pumpAndSettle();
expect(
find.text('Dashboard'),
findsOneWidget,
);
});
Testing Logout Functionality
testWidgets('Logout returns user to login screen', (tester) async {
await tester.pumpWidget(
const LoggedInApp(),
);
await tester.tap(
find.text('Logout'),
);
await tester.pumpAndSettle();
expect(
find.text('Login'),
findsOneWidget,
);
});
Testing Shopping Cart Functionality
testWidgets('Product can be added to cart', (tester) async {
await tester.pumpWidget(
const ShoppingApp(),
);
await tester.tap(
find.text('Add to Cart'),
);
await tester.pump();
expect(
find.text('1 item'),
findsOneWidget,
);
});
Testing Search Functionality
testWidgets('Search filters products', (tester) async {
await tester.pumpWidget(
const ProductSearchScreen(),
);
await tester.enterText(
find.byKey(
const ValueKey('searchField'),
),
'Laptop',
);
await tester.pump();
expect(
find.text('Laptop'),
findsOneWidget,
);
});
Testing Validation Messages
| Input | Expected Result |
|---|
| Empty email | Email validation message |
| Invalid email | Invalid email message |
| Empty password | Password validation message |
| Valid email and password | Form submission proceeds |
Arrange, Act, Assert Pattern
A useful way to structure UI and functionality tests is the Arrange, Act, Assert pattern.
Arrange
↓
Build application and prepare data
Act
↓
Perform user interaction
Assert
↓
Verify expected UI or functionality
Example
testWidgets('Counter increments', (tester) async {
// Arrange
await tester.pumpWidget(
const CounterApp(),
);
// Act
await tester.tap(
find.byKey(
const ValueKey('incrementButton'),
),
);
await tester.pump();
// Assert
expect(
find.text('1'),
findsOneWidget,
);
});
Unit vs UI vs Integration Testing
| Feature | Unit Test | Widget Test | Integration Test |
|---|
| Business logic | Excellent | Possible | Possible |
| UI rendering | No | Yes | Yes |
| User interaction | No | Yes | Yes |
| Navigation | No | Yes | Yes |
| Complete workflow | No | Limited | Yes |
| Speed | Fast | Fast | Slower |
| Environment | Dart test environment | Flutter test environment | Device or emulator |
Testing Complete User Flow
Open Application
↓
Login Screen
↓
Enter Email
↓
Enter Password
↓
Tap Login
↓
Wait for Result
↓
Dashboard
↓
Open Product
↓
Add to Cart
↓
Open Cart
↓
Checkout
Integration Testing UI and Functionality
Integration tests verify that multiple parts of the application work together. They are useful for important end-to-end workflows where UI, application logic, services, and other components interact.
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Complete application workflow', (tester) async {
await tester.pumpWidget(
const MyApp(),
);
expect(
find.text('0'),
findsOneWidget,
);
await tester.tap(
find.byKey(
const ValueKey('increment'),
),
);
await tester.pumpAndSettle();
expect(
find.text('1'),
findsOneWidget,
);
});
}
Testing Workflow Best Practice
Requirement
↓
Identify UI
↓
Identify User Action
↓
Identify Expected Result
↓
Write Test
↓
Run Test
↓
Analyze Failure
↓
Fix Code
↓
Run Again
Common UI Testing Mistakes
- Testing only whether widgets exist without testing their behavior.
- Not verifying the result after a user action.
- Forgetting to call
pump() after state-changing interactions.
- Using
pumpAndSettle() blindly when an infinite animation may prevent settling.
- Using fragile text-based finders when a stable key is more appropriate.
- Ignoring loading and error states.
- Ignoring empty states.
- Not testing form validation.
- Not testing navigation.
- Depending unnecessarily on live APIs.
- Writing tests that depend on another test.
- Testing implementation details instead of observable behavior.
Best Practices for Flutter UI Testing
- Test behavior that matters to users.
- Keep individual tests focused.
- Use descriptive test names.
- Use
find and appropriate Finders to locate widgets.
- Use stable keys for important interactive widgets when appropriate.
- Use
pump() after state-changing interactions.
- Use
pumpAndSettle() when waiting for scheduled frames or animations to finish.
- Test success and failure scenarios.
- Test loading and empty states.
- Test form validation.
- Test navigation and important user journeys.
- Keep external dependencies controlled in automated tests.
- Use integration tests for important end-to-end workflows.
- Run tests regularly during development.
Running Flutter Tests
Flutter tests can be executed from the root directory of a Flutter project.
Run All Tests
flutter test
Run a Specific Test File
flutter test test/widget_test.dart
Run Integration Tests
flutter test integration_test
Analyze the Project
flutter analyze
Test File Structure
my_flutter_app/
├── lib/
│ ├── main.dart
│ ├── screens/
│ ├── widgets/
│ ├── services/
│ └── models/
├── test/
│ ├── unit/
│ ├── widget/
│ └── services/
├── integration_test/
│ └── app_test.dart
└── pubspec.yaml
Practical Complete Example
Counter UI
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State createState() => _CounterPageState();
}
class _CounterPageState extends State {
int counter = 0;
void increment() {
setState(() {
counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Counter'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'$counter',
key: const ValueKey('counterValue'),
),
const SizedBox(height: 20),
ElevatedButton(
key: const ValueKey('incrementButton'),
onPressed: increment,
child: const Text('Increment'),
),
],
),
),
);
}
}
Counter UI Test
testWidgets('Counter functionality works', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: CounterPage(),
),
);
expect(
find.byKey(
const ValueKey('counterValue'),
),
findsOneWidget,
);
expect(
find.text('0'),
findsOneWidget,
);
await tester.tap(
find.byKey(
const ValueKey('incrementButton'),
),
);
await tester.pump();
expect(
find.text('1'),
findsOneWidget,
);
await tester.tap(
find.byKey(
const ValueKey('incrementButton'),
),
);
await tester.pump();
expect(
find.text('2'),
findsOneWidget,
);
});
What This Test Verifies
- The counter screen can be built.
- The counter initially displays
0.
- The increment button exists.
- The increment button can be tapped.
- The UI updates after the tap.
- The counter changes from
0 to 1.
- The counter changes from
1 to 2.
UI and Functionality Testing Checklist
| Area | Testing Checklist |
|---|
| UI | Text, icons, buttons, images, layouts, and widgets display correctly. |
| Buttons | Buttons respond to taps and perform expected actions. |
| Text Fields | Users can enter and update text. |
| Forms | Validation works for valid and invalid input. |
| Navigation | Users can move between screens correctly. |
| Lists | Items display and scrolling works correctly. |
| State | UI updates after state changes. |
| Loading | Loading indicators appear when expected. |
| Error | Error messages appear when operations fail. |
| Empty State | Empty-state UI appears when there is no data. |
| Async Operations | Future and asynchronous UI behavior is handled correctly. |
| Complete Flow | Important end-to-end user journeys work correctly. |
Interview Questions
- What is Flutter UI testing?
- What is functionality testing?
- What is the difference between unit testing and widget testing?
- What is
testWidgets()?
- What is
WidgetTester?
- What is the purpose of
pumpWidget()?
- What is the difference between
pump() and pumpAndSettle()?
- How do you find a widget by text?
- How do you find a widget by key?
- What is
findsOneWidget?
- How do you test a button tap?
- How do you enter text into a TextField during a widget test?
- How do you test navigation in Flutter?
- How do you test form validation?
- How do you test scrolling?
- What is golden testing?
- Why are keys useful in Flutter widget tests?
- How do you test loading and error states?
- What is integration testing?
- Why should external dependencies sometimes be mocked or replaced with fakes?
- What is the Arrange-Act-Assert pattern?
- How do you run Flutter tests from the terminal?
- What are common mistakes when writing Flutter UI tests?
Summary
Testing Flutter UI and Functionality ensures that an application's interface and features behave correctly from the user's perspective. Widget tests are especially useful for checking widgets, text, buttons, forms, navigation, gestures, lists, loading states, and state changes. Unit tests can verify business logic independently, while integration tests can verify complete application workflows. A strong Flutter testing strategy combines these approaches to provide confidence in both individual components and important user journeys.
Learn Flutter
JustAcademy Flutter Training Course
Register for Flutter Course Demo